package org.checkerframework.checker.i18nformatter;

import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey;
import org.checkerframework.checker.formatter.FormatterTreeUtil.InvocationType;
import org.checkerframework.checker.formatter.FormatterTreeUtil.Result;
import org.checkerframework.checker.i18nformatter.I18nFormatterTreeUtil.FormatType;
import org.checkerframework.checker.i18nformatter.I18nFormatterTreeUtil.I18nFormatCall;
import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory;
import org.checkerframework.checker.i18nformatter.qual.I18nFormatFor;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.basetype.BaseTypeVisitor;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.javacutil.AnnotationUtils;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreeUtils;

/**
 * Whenever a method with {@link I18nFormatFor} annotation is invoked, it will perform the format
 * string verification.
 *
 * @checker_framework.manual #i18n-formatter-checker Internationalization Format String Checker
 */
public class I18nFormatterVisitor extends BaseTypeVisitor<I18nFormatterAnnotatedTypeFactory> {

  public I18nFormatterVisitor(BaseTypeChecker checker) {
    super(checker);
  }

  @Override
  public Void visitMethodInvocation(MethodInvocationTree tree, Void p) {
    I18nFormatterTreeUtil tu = atypeFactory.treeUtil;
    I18nFormatCall fc = tu.createFormatForCall(tree, atypeFactory);
    if (fc != null) {
      checkInvocationFormatFor(fc);
      return p;
    } else {
      return super.visitMethodInvocation(tree, p);
    }
  }

  private void checkInvocationFormatFor(I18nFormatCall fc) {
    I18nFormatterTreeUtil tu = atypeFactory.treeUtil;
    Result<FormatType> type = fc.getFormatType();

    switch (type.value()) {
      case I18NINVALID:
        tu.failure(type, "i18nformat.string", fc.getInvalidError());
        break;
      case I18NFORMATFOR:
        if (!fc.isValidFormatForInvocation()) {
          Result<FormatType> failureType = fc.getInvalidInvocationType();
          tu.failure(failureType, "i18nformat.formatfor");
        }
        break;
      case I18NFORMAT:
        Result<InvocationType> invc = fc.getInvocationType();
        I18nConversionCategory[] formatCats = fc.getFormatCategories();
        switch (invc.value()) {
          case VARARG:
            Result<TypeMirror>[] paramTypes = fc.getParamTypes();
            int paraml = paramTypes.length;
            int formatl = formatCats.length;

            // For assignments, "i18nformat.missing.arguments" and
            // "i18nformat.excess.arguments" are
            // issued from commonAssignmentCheck().
            if (paraml < formatl) {
              tu.warning(invc, "i18nformat.missing.arguments", formatl, paraml);
            }
            if (paraml > formatl) {
              tu.warning(invc, "i18nformat.excess.arguments", formatl, paraml);
            }
            for (int i = 0; i < formatl && i < paraml; ++i) {
              I18nConversionCategory formatCat = formatCats[i];
              Result<TypeMirror> param = paramTypes[i];
              TypeMirror paramType = param.value();
              switch (formatCat) {
                case UNUSED:
                  tu.warning(param, "i18nformat.argument.unused", " " + (1 + i));
                  break;
                case GENERAL:
                  break;
                default:
                  if (!fc.isValidParameter(formatCat, paramType)) {
                    ExecutableElement method = TreeUtils.elementFromUse(fc.getTree());
                    CharSequence methodName = ElementUtils.getSimpleDescription(method);
                    tu.failure(
                        param,
                        "argument",
                        "", // parameter name is not useful
                        methodName,
                        paramType,
                        formatCat);
                  }
              }
            }
            break;
          case NULLARRAY:
          // fall-through
          case ARRAY:
            for (I18nConversionCategory cat : formatCats) {
              if (cat == I18nConversionCategory.UNUSED) {
                tu.warning(invc, "i18nformat.argument.unused", "");
              }
            }
            tu.warning(invc, "i18nformat.indirect.arguments");
            break;
          default:
            break;
        }
        break;
      default:
        break;
    }
  }

  @Override
  protected boolean commonAssignmentCheck(
      AnnotatedTypeMirror varType,
      AnnotatedTypeMirror valueType,
      Tree valueTree,
      @CompilerMessageKey String errorKey,
      Object... extraArgs) {
    boolean result = true;

    AnnotationMirror rhs =
        valueType.getPrimaryAnnotationInHierarchy(atypeFactory.I18NUNKNOWNFORMAT);
    AnnotationMirror lhs = varType.getPrimaryAnnotationInHierarchy(atypeFactory.I18NUNKNOWNFORMAT);

    // "i18nformat.missing.arguments" and "i18nformat.excess.arguments" are issued here for
    // assignments.
    // For method calls, they are issued in checkInvocationFormatFor().
    if (rhs != null
        && lhs != null
        && AnnotationUtils.areSameByName(rhs, I18nFormatterAnnotatedTypeFactory.I18NFORMAT_NAME)
        && AnnotationUtils.areSameByName(lhs, I18nFormatterAnnotatedTypeFactory.I18NFORMAT_NAME)) {
      I18nConversionCategory[] rhsArgTypes =
          atypeFactory.treeUtil.formatAnnotationToCategories(rhs);
      I18nConversionCategory[] lhsArgTypes =
          atypeFactory.treeUtil.formatAnnotationToCategories(lhs);

      if (rhsArgTypes.length < lhsArgTypes.length) {
        // From the manual:
        // It is legal to use a format string with fewer format specifiers
        // than required, but a warning is issued.
        checker.reportWarning(
            valueTree, "i18nformat.missing.arguments", varType.toString(), valueType.toString());
      } else if (rhsArgTypes.length > lhsArgTypes.length) {
        // Since it is known that too many conversion categories were provided, issue a more
        // specific error message to that effect than "assignment".
        checker.reportError(
            valueTree, "i18nformat.excess.arguments", varType.toString(), valueType.toString());
        result = false;
      }
    }

    // TODO: What does "take precedence over" mean?  Both are issued, but the
    // "i18nformat.excess.arguments" appears first in the output.  Is this meant to not call
    // super.commonAssignmentCheck() if `result` is already false?
    // By calling super.commonAssignmentCheck() last, any "i18nformat.excess.arguments"
    // message issued for a given line of code will take precedence over the "assignment"
    // issued by super.commonAssignmentCheck().
    result =
        super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs) && result;
    return result;
  }
}
